Skip to content

refactor(adapters): make the git clone credential URL self-contained - #621

Open
Hydralerne wants to merge 5 commits into
mainfrom
fix/git-clone-self-contained-credentials
Open

refactor(adapters): make the git clone credential URL self-contained#621
Hydralerne wants to merge 5 commits into
mainfrom
fix/git-clone-self-contained-credentials

Conversation

@Hydralerne

Copy link
Copy Markdown
Member

Summary

Follow-up to #609. That PR moved non-ghs_ GitHub tokens into the Basic-auth username slot and left the password empty, emitting https://<token>@github.com/…. This keeps that direction but fills both slots, so the URL is a complete credential on its own.

Why

https://<token>@host is not a complete credential to git. Git sends an empty password, and on the 401 goes looking for the real one — GIT_ASKPASS, then the credential helper, then the tty. Verified with a logging askpass:

URL form valid token invalid token
x-access-token:<tok>@ (pre-#609) success, askpass 0 GitHub's error, askpass 0
<tok>@ (#609, on main today) success, askpass 0 GitHub's error, askpass 1
<tok>:x-oauth-basic@ (this PR) success, askpass 0 GitHub's error, askpass 0

It works on main today only because all four clone paths happen to set GIT_ASKPASS=/bin/echo, so the detour ends in a retry with garbage. Strip that one env var and the clone fails with git's own fatal: unable to get password from user before it ever authenticates — GitHub's actual reason never reaches the build log. Reproduced:

$ GIT_TERMINAL_PROMPT=0 git -c credential.helper= clone https://<tok>@github.com/<owner>/<private>.git
fatal: unable to get password from user

Filling both slots removes that dependency. Which slot carries the token is irrelevant to GitHub — it reads the token from either and ignores the other value — so each token type now gets the pair GitHub itself documents: <token>:x-oauth-basic for user tokens (classic PAT, fine-grained, OAuth), x-access-token:<token> for App installation tokens (unchanged from both main and pre-#609).

Changes

  • git-clone.ts — extract gitCredentialPair(hostname, token) so the credential form is decided in one exported, directly-testable place; injectGitToken just applies it.
  • Trim the token. A value pasted with surrounding whitespace percent-encoded into the URL as %20…%20 and failed auth for a reason no log made visible.
  • Match github.com exactly instead of suffix-matching /(^|\.)github\.com$/, which also caught gist./raw. subdomains. Narrowing can only route a host to the general x-access-token form, never leave one without a credential.
  • Correct the comment's premise. It asserted that GitHub rejects a PAT behind x-access-token. It does not — it reads the token from either slot, which is why the pre-fix(adapters): ride GitHub PATs in the username slot of clone URLs #609 form worked too. The comment now documents what is actually load-bearing (self-containment) rather than a mechanism that does not hold.
  • relay.ts — cross-reference only, no behaviour change. Its credential-helper reply legitimately keeps username=x-access-token: that is git's helper protocol, not a URL, and it is a working production path. Its comment previously read as contradicting fix(adapters): ride GitHub PATs in the username slot of clone URLs #609's; it now states the actual rule and points at gitCredentialPair.
  • RedactionURL_USERINFO matches by userinfo position, not by which slot holds the secret, so the new pair was already covered. Added a test pinning it and documented both shapes in the sanitizer doc.

Verification

Live, against GitHub, using the URL the refactored function actually emits and a private repo (token redacted):

emitted shape: https://<TOKEN>:x-oauth-basic@github.com/<owner>/<private>.git
>>> LIVE CLONE OF PRIVATE REPO: SUCCEEDED
>>> askpass invoked: 0 (0 = self-contained)

Gates:

packages/adapters   vitest run    134 files / 2932 tests passing  (+7)
apps/api            build-log-sanitize.test.ts   25 passing  (+1)
packages/adapters   tsc --noEmit  clean
prettier            touched lines clean

New tests cover: ghs_ unchanged, classic/fine-grained/OAuth pairs, a legacy prefix-less 40-char PAT, GitHub Enterprise on its own domain, non-GitHub hosts, gist. subdomain taking the general form, whitespace-only and absent tokens, scp-form untouched, plus a matrix invariant asserting both Basic-auth slots are non-empty for every token type × host.

Note on scope

Pre-existing prettier drift in build-pipeline.ts, index.ts and relay.ts is left untouched — those files were already unformatted on main, and reformatting them here would be the unrelated churn this PR is trying not to add. Only the lines this change introduces are formatted.

Caveat, stated plainly

Nobody has tested x-access-token:<ghp_…> against live GitHub — not #609, not this PR — because GitHub has no API to mint a PAT. The evidence says the username is ignored (community#173881), which means #609 was very likely fixing a misdiagnosis: #607's reporter more plausibly hit an org classic-PAT restriction or a missing scope, neither of which a URL reshape addresses. This PR deliberately does not relitigate that. It keeps #609's shape and makes it robust either way, so it is correct whichever answer the untested case turns out to have.

Hydralerne and others added 5 commits August 18, 2026 16:06
#609 moved non-ghs_ GitHub tokens into the username slot and left the
password empty, which produces `https://<token>@github.com/…`. That is not
a complete credential to git: it sends an empty password and, on the 401,
goes looking for the real one via GIT_ASKPASS, then the credential helper,
then the tty. It works today only because every clone path happens to set
`GIT_ASKPASS=/bin/echo`; strip that and the clone dies with "unable to get
password from user" without ever authenticating, and GitHub's real reason
never reaches the build log.

Keep #609's direction — the token rides the username slot on github.com —
but fill both slots so the URL stands alone on success AND on failure:
`<token>:x-oauth-basic@` for user tokens (the pair GitHub documents for
these), `x-access-token:<token>@` for App installation tokens. Verified
against live GitHub with a private repo: clone succeeds, askpass invoked
zero times, and on a bad token git reports GitHub's message rather than
its own.

Also:
- Extract `gitCredentialPair(hostname, token)` so the credential form is
  decided in one exported, directly-testable place.
- Trim the token. A value pasted with whitespace percent-encoded into the
  URL as %20…%20 and failed auth for a reason no log made visible.
- Match `github.com` exactly instead of suffix-matching `.github.com`,
  which also caught gist./raw. subdomains. Narrowing can only route a host
  to the general x-access-token form, never leave one without a credential.
- Correct the comment's premise. It asserted GitHub rejects a PAT behind
  `x-access-token`; GitHub reads the token from either Basic-auth slot and
  ignores the other value, which is why the pre-#609 form worked too. The
  comment now states what is actually load-bearing (self-containment).
- Cross-reference from relay.ts, whose credential-helper reply legitimately
  keeps `x-access-token` — it is the helper protocol, not a URL. No
  behaviour change there.
- Redaction: the sanitizer matches by userinfo position, not by which slot
  holds the secret, so the new pair is covered. Added a test that pins it
  and documented both shapes.

Tests: adapters 134 files / 2932 passing (+7); api build-log-sanitize 25
passing (+1); `tsc --noEmit` clean. Pre-existing prettier drift in
build-pipeline.ts / index.ts / relay.ts left untouched — only the lines
this change adds are formatted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant